﻿
using EmperialApps.WeatherSpark.Data;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Mail;
using System.Threading;

namespace EmperialApps.WeatherSpark.Service.Internal {

    internal static class Extensions {

        private const string MailAccount = "weatherspark@gmail.com";

        private static readonly BlockingCollection<Pair<MailMessage, Action>> _asyncMailQueue = new BlockingCollection<Pair<MailMessage, Action>>( );
        private static readonly ThreadLocal<SmtpClient> _syncMailClient = new ThreadLocal<SmtpClient>( ( ) => CreateMailClient( async: false ) );

        static Extensions( ) {
            // Begin wait for first asynchronous mail message on separate thread.
            new Thread( ( ) => { CreateMailClient( async: true ); } ) { Name = "WS Asynchronous Mail Starter" }.Start( );
        }

        private static SmtpClient CreateMailClient( bool async ) {
            string password = CloudConfigurationManager.GetSetting( "ErrorReportingPassword" );
            var credentials = new NetworkCredential( MailAccount, password, "" );

            var mailClient = new SmtpClient( "smtp.gmail.com", 587 ) { EnableSsl = true, Credentials = credentials };
            if( async ) {
                SendCompletedEventHandler handler = ( s, e ) => {
                    // Complete previous asynchronous message.
                    if( e != null ) {
                        MailMessage sentMessage;
                        Action completeAction;
                        Pair.TryGetValues( e.UserState, out sentMessage, out completeAction );

                        using( sentMessage ) {
                            if( e.Error == null )
                                TraceEventType.Information.Trace( "[Report] Sent." );
                            else
                                e.Error.Trace( "[Report] Send error" );
                        }

                        if( completeAction != null )
                            completeAction( );
                    }

                    // Wait for next asynchronous message.
                    TraceEventType.Verbose.Trace( "Waiting for next asynchronous mail message." );
                    var pair = _asyncMailQueue.Take( );
                    mailClient.SendAsync( pair.Item1, pair );
                };

                mailClient.SendCompleted += handler;

                TraceEventType.Information.Trace( "Starting asynchronous mail message handler." );
                handler( null, null );
            }

            return mailClient;
        }


        public static void Report( this MailMessage message, string subject, Action complete = null, bool wait = false ) {
            try {
#if DEBUG
                subject = "[DEBUG] " + subject;
#endif
                message.To.Add( MailAccount );
                message.From = message.To[0];
                message.Subject = subject;

                if( wait )
                    using( message ) {
                        _syncMailClient.Value.Send( message );
                        if( complete != null )
                            complete( );
                    }
                else
                    _asyncMailQueue.Add( Pair.Create( message, complete ) );
            }
            catch( Exception ex ) {
                TraceEventType.Error.Trace( "[Report] Creation error: " + ex );
            }
        }

        public static void Report( this Exception error, string subject, bool wait = false ) {
            try {
                error.Trace( subject );

                string footer = new System.Diagnostics.StackTrace( ).ToString( );
                string body = error + Environment.NewLine + "----------" + Environment.NewLine + footer;

                string errorName = error.GetType( ).FullName;
                if( !body.Contains( errorName ) )
                    body = "[" + errorName + "]" + body;

                var message = new MailMessage { Body = body };
                Report( message, subject, null, wait );
            }
            catch( Exception ex ) {
                TraceEventType.Error.Trace( "[Failure report] Creation error: " + ex );
            }
        }


        public static IList<T> GetList<T>( bool empty ) {
            return empty
                 ? EmptyList<T>.Instance
                 : new List<T>( );
        }


        public static T AssertType<T>( this object o ) {
            Debug.Assert( o is T );
            return (T)o;
        }

        public static string ToUniversalString( this DateTime time ) {
            return time.ToString( "u" ).TrimEnd( 'Z' );
        }

        public static void Trace( this TraceEventType category, string message ) {
            string time = DateTime.Now.ToUniversalString( );
            System.Diagnostics.Trace.WriteLine( message, time + " WS " + category );
        }

        public static void Trace( this TraceEventType category, string format, params object[] args ) {
            string message = string.Format( format, args );
            Trace( category, message );
        }

        public static void Trace( this TableResult result, string action, Type entityType ) {
            var statusCode = result.HttpStatusCode;
            var category =
                statusCode / 100 == 2 || (action == "Retrieving" && statusCode == 404)
                    ? TraceEventType.Verbose
                    : TraceEventType.Warning;

#if !DEBUG
            if( category != TraceEventType.Verbose )
#endif
            Trace( category, "{0} {1}: {2} ({3})", action, entityType.Name, (HttpStatusCode)statusCode, (int)statusCode );
        }

        public static void Trace( this Exception error, string message ) {
            Trace( TraceEventType.Error, message + ": " + error );
            System.Diagnostics.Trace.Flush( );
        }


        public static T Try<T>( this ForecastEventArgs e, Func<Forecast, T> success, Func<Exception, T> failure ) {
            Exception ex = e.Error;
            return ex == null
                 ? success( e.Forecast )
                 : failure( ex );
        }

        public static T Try<T>( this ForecastEventArgs e, Func<Forecast, T> success, T fallback ) {
            return Try<T>( e, success, _ => fallback );
        }


        public static string GetName( this Coordinate location ) {
            return location.ToString( display: false );
        }

        public static bool? HasRedirect( this Forecast forecast ) {
            if( forecast == null )
                return null;

            string redirectUri;
            return forecast.TryGetRedirectUri( out redirectUri );
        }


        public static bool IsTimeoutException( this StorageException ex ) {
            if( ex == null )
                return false;

            if( ex.InnerException is TimeoutException )
                return true;

            return ex.InnerException is WebException
                && ex.InnerException.Message.StartsWith( "The operation has timed out", StringComparison.Ordinal );
        }

        public static T Retrieve<T>( this CloudTable table, T entity ) where T : TableEntity {
            var operation = TableOperation.Retrieve<T>( entity.PartitionKey, entity.RowKey );
            try {
                return RetreveCore<T>( table, operation );
            }
            catch( StorageException ex ) {
                // Allow one retry if operation times out.
                if( IsTimeoutException( ex ) || ex.InnerException is System.IO.IOException || ex.InnerException is WebException )
                    return RetreveCore<T>( table, operation );
                else
                    throw;
            }
        }

        private static T RetreveCore<T>( CloudTable table, TableOperation operation ) where T : TableEntity {
            var response = table.Execute( operation );
            response.Trace( "Retrieving", typeof( T ) );

            return (T)response.Result;
        }

    }

}
